# Ornatrix user-defined brush script
import ephere_ornatrix as ox
import math

# User-defined brush name, shown in the drop-down list for brush selection. If not defined, the file name will be used
BrushName = 'Demo Clump'


class Polyline( object ):
	def __init__( self, vertices ):
		self.vertices = vertices

	def __len__( self ):
		return len( self.vertices )

	def GetParametricValue( self, param ):
		"""Returns a vertex lying on the polyline at the value of param (between 0 and 1)"""
		pointCount = len( self.vertices )
		scaledParam = param * ( pointCount - 1 )
		index1 = int( math.floor( scaledParam ) )
		if index1 == pointCount - 1:
			index2 = index1
			index1 = index1 - 1
		else:
			index2 = index1 + 1
		residualParam = scaledParam - index1
		return self.vertices[index1] * ( 1 - residualParam ) + self.vertices[index2] * residualParam

	def InterpolateToPolyline( self, polyline, interpolationCoefficientFunction, interpolationMultiplier ):
		result = []
		vertexCount = len( self.vertices )
		for vertexIndex in range( vertexCount ):
			param = float( vertexIndex ) / ( vertexCount - 1 )
			interpCoeff = interpolationCoefficientFunction( param ) * interpolationMultiplier
			result.append( self.vertices[vertexIndex] * ( 1 - interpCoeff ) + polyline.GetParametricValue( param ) * interpCoeff )
		return result


class Strand( object ):
	def __init__( self, hair, strandIndex, brushStrength ):
		self.originalVertices = Polyline( hair.GetStrandPoints( strandIndex, ox.CoordinateSpace.Object ) )
		self.count = len( self.originalVertices )
		self.modifiedVertices = None
		self.strandIndex = strandIndex
		self.brushStrength = brushStrength

	def GetParametricValue( self, param ):
		return self.originalVertices.GetParametricValue( param )

	def InterpolateToPolyline( self, polyline, interpolationCoefficientFunction ):
		self.modifiedVertices = self.originalVertices.InterpolateToPolyline( polyline, interpolationCoefficientFunction, self.brushStrength )


class HairProcessor( object ):
	def __init__( self, hair, strandIndices, brushParameters, brushStrokeLength ):
		self.hair = hair
		self.strands = []
		self.strandCount = len( strandIndices )
		self.maxPointCount = 0
		# make sure we never interpolate with coefficient > 1
		self.brushStrength = min( brushParameters.GetStrength() * brushStrokeLength, 1 )
		# reverse sign if selected in the GUI
		self.brushStrength *= -1 if brushParameters.reverseSign else 1
		for strandIndex in strandIndices:
			strength = self.brushStrength if not brushParameters.affectSelectedOnly else self.brushStrength * hair.GetStrandChannelData( 0, strandIndex )
			newStrand = Strand( hair, strandIndex, strength )
			self.strands.append( newStrand )
			if newStrand.count > self.maxPointCount:
				self.maxPointCount = newStrand.count
		self.strandCount = len( self.strands )

	def GetAverageStrand( self ):
		result = [ ox.Vector3( 0, 0, 0 ) for index in range( self.maxPointCount ) ]
		for strand in self.strands:
			for resultVertexIndex in range( self.maxPointCount ):
				strandParam = float( resultVertexIndex ) / ( self.maxPointCount - 1 )
				result[resultVertexIndex] += strand.GetParametricValue( strandParam ) / self.strandCount
		return result

	def UpdateHair( self ):
		for strand in self.strands:
			if strand.modifiedVertices is not None:
				self.hair.SetStrandPoints( strand.strandIndex, strand.modifiedVertices, ox.CoordinateSpace.Object )

	def Clump( self, interpolationCoefficientFunction ):
		average = Polyline( self.GetAverageStrand() )
		for strand in self.strands:
			strand.InterpolateToPolyline( average, interpolationCoefficientFunction )


def Evaluate( guidesEditor, brushParameters, brushDirectionInObjectSpace, brushStrokeLength, strandIndices ):
	"""Applies the brush on a list of strands"""
	# print( 'User-defined brush {0} acting with direction {1} and stroke length {2}\nStrand list: {3}\n'.format( BrushName, brushDirectionInObjectSpace, brushStrokeLength, strandIndices ) )

	hair = guidesEditor.GetEditableGuides()
	hairProcessor = HairProcessor( hair, strandIndices, brushParameters, brushStrokeLength )
	#hairProcessor.Clump( lambda param: param )
	strandStrengthAttenuationFunction = brushParameters.GetStrandStrengthAttenuationDiagram()
	hairProcessor.Clump( lambda param: strandStrengthAttenuationFunction.Evaluate( param ) )
	hairProcessor.UpdateHair()
